/*
 * Galaxium Messenger
 * Copyright (C) 2005-2007 Philippe Durand <draekz@gmail.com>
 * Copyright (C) 2005-2007 Ben Motmans <ben.motmans@gmail.com>
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web.Services.Protocols;
using System.Xml;

using Anculus.Core;

using Galaxium.Client;
using Galaxium.Core;
using Galaxium.Gui;
using Galaxium.Protocol.Msn.Soap;

namespace Galaxium.Protocol.Msn
{
	public class MsnConversation : AbstractConversation, IComparable<MsnConversation>
	{
		enum ConversationTransport { Switchboard, Notification, OfflineSwitchboard, Offline };
		
		// Milliseconds to wait for an ack, any longer than this and we'll assume the send failed
		const int _ackTimeout = 30000;
		
		public event EventHandler<ActionMessageEventArgs> ActionMessageReceived;
		public event EventHandler<InviteReceivedEventArgs> ActivityInviteReceived;
		public event EventHandler<InkEventArgs> InkReceived;
		//public event TextMessageEventHandler MessageReceived;
		public event EventHandler<ContactEventArgs> NudgeReceived;
		public event EventHandler<ContactEventArgs> TypingReceived;
		public event EventHandler<WinkEventArgs> WinkReceived;
		public event EventHandler<VoiceClipEventArgs> VoiceClipReceived;
		public event EventHandler<InviteReceivedEventArgs> WebcamInviteReceived;
		
		public event EventHandler<ContactEventArgs> InviteContactFailed;
		public event EventHandler<InkEventArgs> InkSendFailed;
		public event EventHandler<TextMessageEventArgs> MessageSendFailed;
		public event EventHandler<ContactEventArgs> NudgeSendFailed;
		public event EventHandler<WinkEventArgs> WinkSendFailed;
		public event EventHandler<VoiceClipEventArgs> VoiceClipSendFailed;
		
		public event EventHandler CapabilitiesChanged;
		
		delegate void GotLockKeyCallback ();
		
		string _id;
		bool _listenerReady = false;
		Queue<IMsnContent> _inQueue = new Queue<IMsnContent> ();
		SBConnection _switchboard = null;
		ConversationTransport _transport = ConversationTransport.Switchboard;
		List<IEmoticon> _lastEmots;
		
		Dictionary<ContentCommand, DateTime> _sentWaitingAck = new Dictionary<ContentCommand, DateTime> ();
		ContentCommand _ackNextCmd;
		uint _ackTimerHandle;
		
		Guid _offlineRunID = Guid.NewGuid ();
		int _offlineNum;
		
		uint _typingTimerId;
		
		public override bool Active
		{
			get { return _switchboard != null; }
			set { base.Active = value; }
		}
		
		public bool CanInvite
		{
			get { return _transport == ConversationTransport.Switchboard; }
		}
		
		public bool CanSendActionMessage
		{
			get { return true; }
		}
		
		public bool CanSendEmoticons
		{
			get { return _transport == ConversationTransport.Switchboard; }
		}
		
		public bool CanSendMessage
		{
			get
			{
				if ((_transport == ConversationTransport.Notification) && (!ContactsOnline))
					return false;
				
				return true;
			}
		}
		
		public bool CanSendNudge
		{
			get
			{
				if ((_transport == ConversationTransport.Notification) && (!ContactsOnline))
					return false;
				
				return (_transport != ConversationTransport.Offline) && (_transport != ConversationTransport.OfflineSwitchboard);
			}
		}
		
		public bool CanSendTyping
		{
			get { return (_transport != ConversationTransport.Offline) && (_transport != ConversationTransport.OfflineSwitchboard); }
		}
		
		public bool CanSendWink
		{
			get { return _transport == ConversationTransport.Switchboard; }
		}
		
		public bool CanSendVoiceClip
		{
			get { return _transport == ConversationTransport.Switchboard; }
		}
		
		public bool CanStartActivity
		{
			get { return _transport == ConversationTransport.Switchboard; }
		}
		
		public string ID
		{
			get { return _id; }
		}
		
		public bool ListenerReady
		{
			get { return _listenerReady; }
			set
			{
				//Anculus.Core.Log.Debug ("ListenerReady now {0}", value);
				
				_listenerReady = value;
				ProcessInQueue ();
			}
		}
		
		public string ActivityPageUrl
		{
			get
			{
				(Session as MsnSession).ClientConfig.EnsureConfig (null);
				
				if ((Session as MsnSession).ClientConfig.ConfigRoot == null)
				{
					Log.Warn ("Unable to retrieve client config");
					return string.Empty;
				}
				
				XmlElement xml = MsnXmlUtility.FindElement ((Session as MsnSession).ClientConfig.ConfigRoot, "LocalizedConfig/AppDirConfig/AppDirPageURL");
				
				if (xml == null)
				{
					Log.Warn ("Unable to find application directory page config");
					return string.Empty;
				}
				
				return xml.InnerText;
			}
		}
		
		internal SBConnection Switchboard
		{
			get { return _switchboard; }
		}
		
		bool ContactsOnline
		{
			get
			{
				bool ret = true;
				
				foreach (MsnContact contact in _contacts)
					ret &= contact.Presence != MsnPresence.Offline;
				
				return ret;
			}
		}
		
		public bool Typing
		{
			get { return _typingTimerId != 0; }
			set
			{
				if ((!value) && (_typingTimerId != 0))
				{
					// Stop the timer
					TimerUtility.RemoveCallback (_typingTimerId);
					_typingTimerId = 0;
				}
				else if (value && (_typingTimerId == 0))
				{
					SendTyping ();
					_typingTimerId = TimerUtility.RequestInfiniteCallback (TypingTimerCallback, 2000);
				}
			}
		}
		
		public MsnConversation (params MsnContact[] contacts)
			: base (contacts[0], contacts[0].Session)
		{
			_id = Guid.NewGuid ().ToString ();
			
			lock (_contacts)
			{
				foreach (MsnContact contact in contacts)
				{
					_contacts.Add (contact);
					AddContactEventHandlers (contact);
				}
			}
			
			SelectTransport ();
		}
		
		// Constructor called when a switchboard receives content
		public MsnConversation (SBConnection sb)
			: base (sb.Contacts[0], sb.Session)
		{
			Activate (sb);
		}
		
		// Constructor called when an OIM is received
		public MsnConversation (MsnContact contact, string id)
			: base (contact, contact.Session)
		{
			_id = id;
			SelectTransport ();
		}
		
		internal void Activate (SBConnection sb)
		{
			_switchboard = sb;
			_id = sb.ID;
			SetTransport (ConversationTransport.Switchboard);
			
			foreach (MsnContact contact in sb.Contacts)
			{
				// Contact could already be present if reactivating an inactive conversation
				
				if (!_contacts.Contains (contact))
					_contacts.Add (contact);
			}
						
			SetupSwitchboard ();
		}
		
		// Select the way in which we'll send content to contacts
		void SelectTransport ()
		{
			bool _contactWindowsLive = true;
			lock (ContactCollection)
			{
				foreach (MsnContact contact in ContactCollection)
					_contactWindowsLive &= contact.Network == Network.WindowsLive;
			}
			
			if (_contactWindowsLive)
			{
				// If the contact is offline, WLM always tries a switchboard
				// and falls back to sending via OIMs if the invite fails.
				// We'll do the same with OfflineSwitchboard
				
				if (ContactsOnline)
					SetTransport (ConversationTransport.Switchboard);
				else
					SetTransport (ConversationTransport.OfflineSwitchboard);
			}
			else
				SetTransport (ConversationTransport.Notification);
		}
		
		void SetTransport (ConversationTransport newTransport)
		{
			ThreadUtility.Check ();
			
			if (_transport == newTransport)
				return;
			
			Log.Debug ("Conversation {0} switching to {1} transport", _id, newTransport);
			
			_transport = newTransport;
			OnCapabilitiesChanged ();
		}
		
		// We need a switchboard, so ensure one is setup
		void NeedSwitchboard ()
		{
			ThreadUtility.Check ();
			
			ThrowUtility.ThrowIfFalse ("NeedSwitchboard called when not using a switchboard???", (_transport == ConversationTransport.Switchboard) || (_transport == ConversationTransport.OfflineSwitchboard));
			
			if (_switchboard != null)
				return;
			
			// We don't already have a switchboard, so create one
			
			// If everyone has left, we invite the primary contact again
			lock (_contacts)
			{
				if (ContactCollection.Count == 0)
					_contacts.Add (PrimaryContact);
				
				MsnContact[] contacts = new MsnContact[ContactCollection.Count];
				ContactCollection.CopyTo (contacts, 0);
			
				//Log.Debug ("Calling GETSWITCHBOARD from the MSNConversation");
				_switchboard = (Session as MsnSession).GetSwitchboard (true, contacts);
				
				SetupSwitchboard ();
			}
		}
		
		// Setup event handlers we need
		void SetupSwitchboard ()
		{
			_switchboard.Established += OnSBEstablished;
			_switchboard.Closed += OnSBClosed;
			_switchboard.ContactJoined += OnSBContactJoined;
			_switchboard.ContactLeft += OnSBContactLeft;
			_switchboard.InviteContactFailed += OnSBInviteContactFailed;
			
			_switchboard.AddContentHandler (typeof (ControlContent), new ContentHandler (ProcessContent));
			_switchboard.AddContentHandler (typeof (DataCastContent), new ContentHandler (ProcessContent));
			_switchboard.AddContentHandler (typeof (EmoticonContent), new ContentHandler (ProcessContent));
			_switchboard.AddContentHandler (typeof (PlainTextContent), new ContentHandler (ProcessContent));
		}
		
		void OnSBEstablished (object sender, EventArgs args)
		{
			base.OnEstablished (new ConversationEventArgs (this));
		}
		
		void OnSBClosed (object sender, EventArgs args)
		{
			if (!_switchboard.Reconnecting)
			{
				Anculus.Core.Log.Debug ("Switchboard closed, cleaning up");
				
				_switchboard.Established -= OnSBEstablished;
				_switchboard.Closed -= OnSBClosed;
				_switchboard.ContactJoined -= OnSBContactJoined;
				_switchboard.ContactLeft -= OnSBContactLeft;
				_switchboard.InviteContactFailed -= OnSBInviteContactFailed;
				
				_switchboard.RemoveContentHandler (typeof (ControlContent), new ContentHandler (ProcessContent));
				_switchboard.RemoveContentHandler (typeof (DataCastContent), new ContentHandler (ProcessContent));
				_switchboard.RemoveContentHandler (typeof (EmoticonContent), new ContentHandler (ProcessContent));
				_switchboard.RemoveContentHandler (typeof (PlainTextContent), new ContentHandler (ProcessContent));
				
				lock (_sentWaitingAck)
				{
					while (_switchboard.OutQueue.Count > 0)
					{
						IMsnCommand cmd = _switchboard.OutQueue[0];
						_switchboard.OutQueue.RemoveAt (0);
						
						if (cmd is ContentCommand)
						{
							if (_sentWaitingAck.ContainsKey (cmd as ContentCommand))
								_sentWaitingAck.Remove (cmd as ContentCommand);
							
							OnSendFailed (cmd as ContentCommand);
						}
					}
				}
				
				_switchboard = null;
				
				// If we've fallen back to OIMs, a closed switchboard shouldn't change that
				if (_transport != ConversationTransport.Offline)
					SelectTransport ();
				
				base.OnClosed (new ConversationEventArgs (this));
			}
			else
				Log.Debug ("Switchboard reconnecting");
		}
		
		void OnSBInviteContactFailed (object sender, ContactEventArgs args)
		{
			Log.Debug ("Failed to invite {0}", args.Contact.UniqueIdentifier);
			
			OnInviteContactFailed (args);
			
			if (_contacts.Contains (args.Contact))
			{
				// This was one of the contacts present before the switchboard was created
				
				// Remove the contact
				_contacts.Remove (args.Contact);
				RemoveContactEventHandlers (args.Contact);
				
				if (_contacts.Count == 0)
				{
					// This was the only contact in the switchboard/conversation
					
					if (args.Contact.Presence == MsnPresence.Offline)
					{
						// Fallback to sending OIMs
						
						SetTransport (ConversationTransport.Offline);
						
						// We still need the contact
						// Event handlers should still be present because this contact must be primary
						_contacts.Add (args.Contact);
						
						List<IMsnCommand> local = new List<IMsnCommand> (_switchboard.OutQueue);
						foreach (IMsnCommand cmd in local)
						{
							if (!(cmd is ContentCommand))
								continue;
							
							_switchboard.OutQueue.Remove (cmd);
							
							if (_sentWaitingAck.ContainsKey (cmd as ContentCommand))
								_sentWaitingAck.Remove (cmd as ContentCommand);
							
							IMsnContent content = (cmd as ContentCommand).Content;
							Send (content);
						}
					}
					
					// We don't need the switchboard anymore
					_switchboard.Disconnect ();
				}
			}
		}
		
		void OnSBContactJoined (object sender, ContactEventArgs args)
		{
			lock (ContactCollection)
			{
				// This can happen if we've just opened the switchboard
				if (ContactCollection.Contains (args.Contact))
					return;
				
				ContactCollection.Add (args.Contact);
			}
			
			AddContactEventHandlers (args.Contact);

			OnContactJoined (new ContactActionEventArgs (Session, this, args.Contact));
			OnCapabilitiesChanged ();
		}
		
		void OnSBContactLeft (object sender, ContactEventArgs args)
		{
			if (args.Contact == PrimaryContact)
			{
				if (ContactCollection.Count > 1)
				{
					// The primary contact left, switch the primary contact to someone else
					// This also makes sure if this conversation opens a new switchboard we
					// invite the last contact, rather than the original primary who left
					
					foreach (MsnContact contact in ContactCollection)
					{
						_primaryContact = contact;
						break;
					}
				}
				else
				{
					// Don't remove if this is the primary contact and will remain so
					
					SelectTransport ();
					OnCapabilitiesChanged ();
					return;
				}
			}
			

			lock (ContactCollection)
				ContactCollection.Remove (args.Contact);
			
			RemoveContactEventHandlers (args.Contact);
			
			OnContactLeft (new ContactActionEventArgs (Session, this, args.Contact));
			SelectTransport ();
			OnCapabilitiesChanged ();
			
			if (ContactCollection.Count == 0)
				OnAllContactsLeft (new ConversationEventArgs (this));
		}
		
		void AddContactEventHandlers (IContact contact)
		{
			contact.PresenceChange += ContactPresenceChange;
		}
		
		void RemoveContactEventHandlers (IContact contact)
		{
			if (contact != _primaryContact)
				contact.PresenceChange -= ContactPresenceChange;
		}
		
		void ContactPresenceChange (object sender, EntityChangeEventArgs<IPresence> args)
		{
			SelectTransport ();
			OnCapabilitiesChanged ();
		}
		
		public override void InviteContact (IContact contact)
		{
			if (!CanInvite)
				return;
			
			if (_contacts.Contains (contact))
			{
				Log.Warn ("Attempted to invite a contact already in the conversation {0}", contact.UniqueIdentifier);
				return;
			}
			
			NeedSwitchboard ();
			_switchboard.Invite (contact as MsnContact);
		}
		
		public override void Close ()
		{
			if (_ackTimerHandle != 0)
			{
				TimerUtility.RemoveCallback (_ackTimerHandle);
				_ackTimerHandle = 0;
			}
			
			// Don't close the switchboard unless it was a multi-person conversation!
			// It might be in use elsewhere (eg. P2P)
			
			if ((_contacts.Count > 1) && (_switchboard != null))
				_switchboard.Disconnect ();
		}
		
#region Send Methods
		public void Send (IMsnContent content)
		{
			if (_transport == ConversationTransport.Notification)
			{
				// Send message via notification server using UUM
				// eg. We're sending to a yahoo contact
				
				lock (ContactCollection)
				{
					foreach (MsnContact contact in ContactCollection)
					{
						UUMCommand cmd = content.ToCommand<UUMCommand> ();
						cmd.Destination = contact;
						
						//TODO: Can we set the UUM type some other way?
						// I don't like it but can't think of anything better
						if (content is PlainTextContent)
							cmd.Type = UUMType.TextMessage;
						else if (content is ControlContent)
							cmd.Type = UUMType.TypingUser;
						else
							cmd.Type = UUMType.Nudge;
						
						(Session as MsnSession).Connection.Send (cmd, delegate (IMsnCommand response)
						{
							if (response is NAKCommand)
								OnSendFailed (cmd);
						});
					}
				}
			}
			else if (_transport == ConversationTransport.Offline)
			{
				// Contacts are not online, we need to send using Offline IM

				SendOIM (content);
			}
			else
			{
				// Contacts are all capable of using a switchboard
				// and all are online, so send over switchboard using MSG
				
				NeedSwitchboard ();
				
				// Don't bother sending control content (typing notification) if
				// the switchboard isn't ready yet
				if ((content is ControlContent) && (!_switchboard.CanSend))
					return;
				
				MSGCommand cmd = content.ToCommand<MSGCommand> ();
				
				if (cmd.AckType == MSGAckType.Always)
				{
					lock (_sentWaitingAck)
					{
						//Log.Debug ("Sending message we excpect an ACK for");
						
						_sentWaitingAck.Add (cmd, DateTime.Now.AddMilliseconds (_ackTimeout));
						UpdateAckTimer ();
					}
				}
				
				_switchboard.Send (cmd, delegate (IMsnCommand response)
				{
					lock (_sentWaitingAck)
					{
						//Log.Debug ("Received response, removing command from _sentWaitingAck list");
						
						if (_sentWaitingAck.ContainsKey (cmd))
							_sentWaitingAck.Remove (cmd);
					}

					UpdateAckTimer ();
					
					if (response is NAKCommand)
						OnSendFailed (cmd);
				});
			}
		}
		
		public void SendActionMessage (string msg)
		{
			if (!CanSendActionMessage)
				return;
			
			DataCastContent content = new DataCastContent (Session as MsnSession, 4);
			content.MIMEBody["Data"] = msg;
			
			Send (content);
		}
		
		void SendEmoticons (MsnTextMessage msg)
		{
			if (!CanSendEmoticons)
				return;
			
			List<MsnEmoticon> emoticons = new List<MsnEmoticon> ();
			
			foreach (ITextChunk chunk in msg.Chunks)
			{
				if (chunk.Type != TextChunkType.Emoticon)
					continue;
				
				IEmoticon emot = ((EmoticonTextChunk)chunk).Emoticon;
				
				if (EmoticonUtility.IsStandard (emot))
					continue;
				
				if (emot is MsnEmoticon)
					emoticons.Add (emot as MsnEmoticon);
				else
					emoticons.Add (new MsnEmoticon (Session as MsnSession, emot));
			}
			
			if (emoticons.Count > 0)
			{
				EmoticonContent content = new EmoticonContent (Session as MsnSession);
				content.Emoticons = emoticons;
				
				Send (content);
			}
		}
		
		public void SendMessage (MsnTextMessage msg)
		{
			if (!CanSendMessage)
				return;
			
			SendEmoticons (msg);
			
			PlainTextContent content = new PlainTextContent (Session as MsnSession);
			content.Message = msg;
			
			Send (content);
			
			LogMessage (msg);
		}
		
		public void SendNudge ()
		{
			if (!CanSendNudge)
				return;
			
			DataCastContent content = new DataCastContent (Session as MsnSession, 1);
			
			Send (content);
		}
		
		public void SendTyping ()
		{
			if (!CanSendTyping)
				return;
			
			ControlContent content = new ControlContent (Session as MsnSession);
			
			Send (content);
		}
		
		public void SendWink (MsnWink wink)
		{
			if (!CanSendWink)
				return;
			
			DataCastContent content = new DataCastContent (Session as MsnSession, 2);
			content.MIMEBody["Data"] = wink.Context;
			
			Send (content);
		}
#endregion
		
		internal void ProcessContent (IMsnContent content)
		{
			//Anculus.Core.Log.Debug ("Process {0}, {1}", content.GetType ().Name, _listenerReady ? "ready" : "not ready");
			
			if (content is PlainTextContent)
			{
				ReceivedMessageActivity activity = new ReceivedMessageActivity (this, (content as PlainTextContent).Message);
				ActivityUtility.EmitActivity (this, activity);
				
				// If an activity handler sets this to true, we don't display the message
				// In all likelihood, this wasn't actually a regular message, rather
				// something like a Messenger Plus! sound which has been handled elsewhere
				
				//Anculus.Core.Log.Debug ("Activity Handled? {0}", activity.Handled);
				
				if (activity.Handled)
				{
					Log.Debug ("Activity handled, dropping message");
					return;
				}
			}
			else if (content is DataCastContent)
			{
				DataCastContent cast = content as DataCastContent;
				
				if (cast.ID == 1)
				{
					ActivityUtility.EmitActivity (this, new ReceivedNudgeActivity (this));
				}
				else if (cast.ID == 2)
				{
					MsnWink wink = MsnObject.Load (Session as MsnSession, cast.MIMEBody["Data"]) as MsnWink;
					ActivityUtility.EmitActivity (this, new ReceivedWinkActivity (this, wink));
				}
				else if (cast.ID == 3)
				{
					MsnVoiceClip clip = MsnObject.Load (Session as MsnSession, cast.MIMEBody["Data"]) as MsnVoiceClip;
					ActivityUtility.EmitActivity (this, new ReceivedVoiceClipActivity (this, clip));
				}
			}
			
			_inQueue.Enqueue (content);
			ProcessInQueue ();
		}
		
		void ProcessInQueue ()
		{
			if (!_listenerReady)
				return;
			
			while (_inQueue.Count > 0)
			{
				IMsnContent content = _inQueue.Dequeue ();
				
				//Anculus.Core.Log.Debug ("Processing {0} from queue", content);
				
				if (content is PlainTextContent)
					OnPlainTextContentReceived (content as PlainTextContent);
				else if (content is EmoticonContent)
					OnEmoticonContentReceived (content as EmoticonContent);
				else if (content is ControlContent)
					OnControlContentReceived (content as ControlContent);
				else if (content is DataCastContent)
					OnDataCastContentReceived (content as DataCastContent);
			}
		}
		
#region Content Handlers
		protected void OnPlainTextContentReceived (PlainTextContent content)
		{
			if (_lastEmots != null)
			{
				(content.Message as MsnTextMessage).CustomEmoticons = _lastEmots;
				_lastEmots = null;
			}
			
			ITextMessage msg = content.Message;
			OnMessageReceived (new TextMessageEventArgs (msg));
			LogMessage (msg);
		}
		
		protected void OnEmoticonContentReceived (EmoticonContent content)
		{
			_lastEmots = new List<IEmoticon> (content.Emoticons.ToArray ());
			
			foreach (MsnEmoticon emot in _lastEmots)
				emot.Request ();
		}
		
		protected void OnControlContentReceived (ControlContent content)
		{
			OnTypingReceived (new ContactEventArgs (content.Source as MsnContact));
		}
		
		protected void OnDataCastContentReceived (DataCastContent content)
		{
			if (content.ID == 1)
			{
				// Nudge
				
				OnNudgeReceived (new ContactEventArgs (content.Source as MsnContact));
			}
			else if (content.ID == 2)
			{
				// Wink
				
				MsnWink wink = MsnObject.Load (Session as MsnSession, content.MIMEBody["Data"]) as MsnWink;
				
				if (wink == null)
				{
					Log.Warn ("Received wink but unable to parse context");
					return;
				}
				
				OnWinkReceived (new WinkEventArgs (wink));
			}
			else if (content.ID == 3)
			{
				// Voice clip
				
				MsnVoiceClip clip = MsnObject.Load (Session as MsnSession, content.MIMEBody["Data"]) as MsnVoiceClip;
				
				if (clip == null)
				{
					Log.Warn ("Received voice clip but unable to parse context");
					return;
				}
				
				OnVoiceClipReceived (new VoiceClipEventArgs (clip));
			}
			else if (content.ID == 4)
			{
				// Action Message
				
				OnActionMessageReceived (new ActionMessageEventArgs (content.MIMEBody["Data"].Value));
			}
			else
				Log.Warn ("Unknown DataCast Received: {0}", content.ID);
		}
#endregion
		
#region Event Raisers
		protected void OnInviteContactFailed (ContactEventArgs args)
		{
			if (InviteContactFailed != null)
				InviteContactFailed (this, args);
		}
		
		protected void OnCapabilitiesChanged ()
		{
			if (CapabilitiesChanged != null)
				CapabilitiesChanged (this, EventArgs.Empty);
		}
		
		protected void OnActionMessageReceived (ActionMessageEventArgs args)
		{
			LogEvent (DateTime.Now, args.Message);
			
			if (ActionMessageReceived != null)
				ActionMessageReceived (this, args);
		}
		
		protected void OnActivityInviteReceived (InviteReceivedEventArgs args)
		{
			if (ActivityInviteReceived != null)
				ActivityInviteReceived (this, args);
		}
		
		protected void OnInkReceived (InkEventArgs args)
		{
			if (InkReceived != null)
				InkReceived (this, args);
		}
		
		protected void OnNudgeReceived (ContactEventArgs args)
		{
			if (NudgeReceived != null)
				NudgeReceived (this, args);
		}
		
		protected void OnSeeWebcamInviteReceived (InviteReceivedEventArgs args)
		{
			if (WebcamInviteReceived != null)
				WebcamInviteReceived (this, args);
		}
		
		protected void OnTypingReceived (ContactEventArgs args)
		{
			if (TypingReceived != null)
				TypingReceived (this, args);
		}
		
		protected void OnWinkReceived (WinkEventArgs args)
		{
			if (WinkReceived != null)
				WinkReceived (this, args);
		}
		
		protected void OnVoiceClipReceived (VoiceClipEventArgs args)
		{
			if (VoiceClipReceived != null)
				VoiceClipReceived (this, args);
		}
		
		protected void OnInkSendFailed (InkEventArgs args)
		{
			if (InkSendFailed != null)
				InkSendFailed (this, args);
		}
		
		protected void OnMessageSendFailed (TextMessageEventArgs args)
		{
			if (MessageSendFailed != null)
				MessageSendFailed (this, args);
		}
		
		protected void OnNudgeSendFailed (ContactEventArgs args)
		{
			if (NudgeSendFailed != null)
				NudgeSendFailed (this, args);
		}
		
		protected void OnWinkSendFailed (WinkEventArgs args)
		{
			if (WinkSendFailed != null)
				WinkSendFailed (this, args);
		}
		
		protected void OnVoiceClipSendFailed (VoiceClipEventArgs args)
		{
			if (VoiceClipSendFailed != null)
				VoiceClipSendFailed (this, args);
		}
#endregion
		
		void OnSendFailed (ContentCommand cmd)
		{
			IMsnContent content = cmd.Content;
			
			if (content == null)
			{
				Anculus.Core.Log.Warn ("Failed to send ContentCommand, but content is null");
				return;
			}
			
			Anculus.Core.Log.Debug ("Failed to send {0}", content);
			
			if (content is PlainTextContent)
				OnMessageSendFailed (new TextMessageEventArgs ((content as PlainTextContent).Message, PrimaryContact));
			else if (content is DataCastContent)
			{
				int id = (content as DataCastContent).ID;
				
				if (id == 1)
					OnNudgeSendFailed (new ContactEventArgs (PrimaryContact));
				else if (id == 2)
					OnWinkSendFailed (new WinkEventArgs (MsnObject.Load (Session as MsnSession, (content as DataCastContent).DataString) as MsnWink));
				else if (id == 3)
					OnVoiceClipSendFailed (new VoiceClipEventArgs (MsnObject.Load (Session as MsnSession, (content as DataCastContent).DataString) as MsnVoiceClip));
			}
		}
		
		internal void EmitActivityInvite (P2PActivity activity)
		{
			Anculus.Core.Log.Debug ("Invited to start '{0}' (AppID {1})", activity.Name, activity.AppID);
			
			OnActivityInviteReceived (new InviteReceivedEventArgs (activity));
		}
		
		internal void EmitSeeWebcamInvite (P2PViewWebcam seecam)
		{
			Anculus.Core.Log.Debug ("Invited to see webcam");
			
			OnSeeWebcamInviteReceived (new InviteReceivedEventArgs (seecam));
		}
		
		public void EmitActionMessage (string msg)
		{
			// Create the content & process it so it gets queued instead of firing off the event
			// immediately
			
			//TODO: I don't like this, it's a bit hackish. The queue for incoming content
			// should be modified to allow queueing without all this mess
			
			DataCastContent content = new DataCastContent (Session as MsnSession, 4);
			content.MIMEBody["Data"] = msg;
			
			ProcessContent (content);
		}
		
#region Ack Timer
		void UpdateAckTimer ()
		{
			lock (_sentWaitingAck)
			{
				if (_sentWaitingAck.Count == 0)
				{
					// Not waiting for any acks
					
					//Log.Debug ("Not expecting any ACKs");
					
					if (_ackTimerHandle != 0)
					{
						TimerUtility.RemoveCallback (_ackTimerHandle);
						_ackTimerHandle = 0;
					}
					
					return;
				}
				
				ContentCommand nextCmd = null;
				
				foreach (ContentCommand cmd in _sentWaitingAck.Keys)
				{
					if ((nextCmd == null) || (_sentWaitingAck[cmd] < _sentWaitingAck[nextCmd]))
						nextCmd = cmd;
				}
				
				if (nextCmd == _ackNextCmd)
				{
					// No change since the last update
					return;
				}
				
				_ackNextCmd = nextCmd;
				
				if (_ackTimerHandle != 0)
				{
					TimerUtility.RemoveCallback (_ackTimerHandle);
					_ackTimerHandle = 0;
				}
				
				int interval = (int)_sentWaitingAck[nextCmd].Subtract (DateTime.Now).TotalMilliseconds;
				//Log.Debug ("Expecting next ACK within {0}ms", interval);
				
				// interval can be negative if it took longer for AckTimerElapsed to process
				// the failures than was left before the next command timed out
				
				if (interval <= 500)
					AckTimerElapsed ();
				else
					_ackTimerHandle = TimerUtility.RequestCallback (AckTimerElapsed, interval);
			}
		}
		
		void AckTimerElapsed ()
		{
			lock (_sentWaitingAck)
			{
				// We can't loop through sentWaitingAck.Keys directly because we'll be modifying it
				List<ContentCommand> local = new List<ContentCommand> (_sentWaitingAck.Keys);
				
				foreach (ContentCommand cmd in local)
				{
					if (_sentWaitingAck[cmd] < DateTime.Now)
					{
						_sentWaitingAck.Remove (cmd);
						
						if ((_switchboard != null) && (_switchboard.OutQueue.Contains (cmd)))
							_switchboard.OutQueue.Remove (cmd);
						
						OnSendFailed (cmd);
					}
				}
			}
			
			//Log.Debug ("Done reporting send failures");
			UpdateAckTimer ();
		}
#endregion
		
#region Offline Messaging
		void SendOIM (IMsnContent content)
		{
			if (!(content is PlainTextContent))
			{
				//TODO: can we send anything other than text?
				OnSendFailed (content.ToCommand<MSGCommand> ());
				return;
			}
			
			(Session as MsnSession).RequireSecurityTokens (new ExceptionDelegate (delegate
			{
				SendOIM (PrimaryContact as MsnContact, content, ++_offlineNum);
				
			}), SecurityToken.Messenger);
		}
		
		void SendOIM (MsnContact contact, IMsnContent content, int num)
		{
			MIMECollection mime = new MIMECollection ();
			mime["MIME-Version"] = "1.0";
			mime["Content-Type"] = "text/plain";
			mime["Content-Type"][" charset"] = "UTF-8";
			mime["Content-Transfer-Encoding"] = "base64";
			mime["X-OIM-Message-Type"] = "OfflineMessage";
			mime["X-OIM-Run-Id"] = _offlineRunID.ToString ("B").ToUpper ();
			mime["X-OIM-Sequence-Num"] = num.ToString ();
			
			string contentStr = mime.ToString () + "\r\n" + EncodingUtility.Base64Encode ((content as PlainTextContent).Message.GetText (), Encoding.UTF8);
			
			(Session as MsnSession).OIMStoreService.toHeader = new Soap.Headers.ToHeader (contact);
			(Session as MsnSession).OIMStoreService.sequenceHeader.MessageNumber = num;
			
			(Session as MsnSession).OIMStoreService.BeginStore2 ("text", contentStr, delegate (IAsyncResult asyncResult)
			{
				try
				{
					(Session as MsnSession).OIMStoreService.EndStore2 (asyncResult);
					
					Anculus.Core.Log.Debug ("Successfully stored OIM");
				}
				catch (SoapException ex)
				{
					if (ex.Code.Name == "AuthenticationFailed")
					{
						if (ex.Detail == null)
						{
							Anculus.Core.Log.Warn ("Unable to find SOAP fault detail, if you're using mono 1.2.4 or older please update");
							OnSendFailed (content.ToCommand<MSGCommand> ());
							return;
						}
						
						string authPolicy = MsnXmlUtility.FindText (ex.Detail as XmlElement, "RequiredAuthPolicy");
						string lockKeyChallenge = MsnXmlUtility.FindText (ex.Detail as XmlElement, "LockKeyChallenge");
						
						if (string.IsNullOrEmpty (lockKeyChallenge))
						{
							Anculus.Core.Log.Error (ex, "Error storing OIM");
							OnSendFailed (content.ToCommand<MSGCommand> ());
							return;
						}
						
						Anculus.Core.Log.Debug ("Got LockKeyChallenge: {0} ({1} auth)", lockKeyChallenge, authPolicy);
						
						(Session as MsnSession).OIMStoreService.ticketHeader.LockKey = new Challenge (MsnConstants.ProductID, MsnConstants.ProductKey).GetChallengeResponse (lockKeyChallenge);
						
						Anculus.Core.Log.Debug ("Calculated LockKey: {0}", (Session as MsnSession).OIMStoreService.ticketHeader.LockKey);
						
						//Send this again
						SendOIM (contact, content, num);
					}
					else
					{
						Anculus.Core.Log.Error ("Error storing OIM: {0}", ex.Code.Name);
						OnSendFailed (content.ToCommand<MSGCommand> ());
					}
				}
				catch (Exception ex)
				{
					Anculus.Core.Log.Error (ex, "Error storing OIM");
					OnSendFailed (content.ToCommand<MSGCommand> ());
				}
			}, null);
		}
#endregion
		
		void TypingTimerCallback ()
		{
			SendTyping ();
		}
		
		public int CompareTo (MsnConversation x)
		{
			return _id.CompareTo (x._id);
		}
	}
}
